﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Needle.Bus.Messages;
using System.Runtime.Serialization;
using System.IO;
using Needle.Bus.Messages.Management;
using System.ComponentModel;
using System.Diagnostics;

namespace Needle.Bus.Engine.MessageHandlers
{
    public class FileReader : IMessageHandler
    {
        Container _readers;

        public void Initialize()
        {
            _readers = new Container();
        }

        public void Process(IMessage message)
        {
            var msg = message as FileReadRequest;
            if (msg == null)
                return;
            try
            {
                if (File.Exists(msg.FileName))
                {
                    StartFileRead(msg);
                }
                else
                {
                    throw new FileNotFoundException("File not found", msg.FileName);
                }
            }
            catch (Exception ex)
            {   // TODO: make a more specific error message
                ErrorMessage errMsg = new ErrorMessage();
                errMsg.CorrelationId = msg.IdForCorrelation;
                errMsg.DestinationAddress = Bus.Context.Stage.Id;
                errMsg.EngineId = Bus.Current.Id;
                errMsg.Error = ex;
                errMsg.Message = msg;
                errMsg.SourceAddress = msg.ReturnAddress;
                errMsg.Timestamp = DateTime.Now;
                Bus.Send(msg.ReturnAddress, errMsg);
            }
        }

        public void Process(IList<IMessage> messages)
        {
            foreach (var msg in messages)
                Process(msg);
        }

        public void Dispose()
        {
            /* TODO: be a little more friendly then
             * pulling the plug on the readers*/
            _readers.Dispose();
        }


        private void StartFileRead(FileReadRequest msg)
        {
            var file = new FileInfo(msg.FileName);
            if (msg.ChunkSize < 1)
                throw new ArgumentException("Chunksize too small.", "ChunkSize");

            int chunkSize = (int)Math.Min(msg.ChunkSize, file.Length);
            int chunkCount = (int)Math.Floor((double)(file.Length / chunkSize));

            var stream = new FileStream(file.FullName,
                             FileMode.Open,
                             FileAccess.Read,
                             FileShare.Read,
                             chunkSize,
                             FileOptions.Asynchronous | FileOptions.SequentialScan);

            var reader = new AsyncReader
            {
                Buffer = new byte[chunkSize],
                ChunkCount = chunkCount,
                ChunkNumber = 0,
                ChunkSize = chunkSize,
                Context = new Bus(Bus.Context.Engine, Bus.Context.Stage),
                Index = 0,
                Request = msg,
                Stream = stream
            };

            _readers.Add(reader);
            
            reader.StartRead();
        }

        private class AsyncReader : Component
        {
            public FileReadRequest Request { get; set; }
            public FileStream Stream { get; set; }
            public byte[] Buffer { get; set; }
            public int ChunkNumber { get; set; }
            public int ChunkCount { get; set; }
            public int ChunkSize { get; set; }
            public int Index { get; set; }
            public Bus Context { get; set; }

            public void StartRead()
            {
                Stream.BeginRead(Buffer, Index, ChunkSize - Index, AsyncFileRead, this);
            }

            private void AsyncFileRead(IAsyncResult result)
            {
                try
                {
                    int bytesRead = Stream.EndRead(result);

                    if (bytesRead == 0)
                    {   /* end of stream */
                        if (Index != 0)
                        {   /* send last chunk */
                            TrimBuffer();
                            SendChunk();
                        }
                        Dispose();
                        return;
                    }
                    else if (Index + bytesRead < ChunkSize)
                    {   /* we didn't read a full buffer */
                        Index += bytesRead;
                        StartRead();
                        return;
                    }

                    SendChunk();
                    NextBuffer();
                    StartRead();
                }
                catch (Exception ex)
                {
                    // TODO: make a more specific error message
                    ErrorMessage errMsg = new ErrorMessage();
                    errMsg.CorrelationId = Request.IdForCorrelation;
                    errMsg.DestinationAddress = Request.ReturnAddress;
                    errMsg.EngineId = Context.Id;
                    errMsg.Error = ex;
                    errMsg.Message = Request;
                    errMsg.SourceAddress = Context.Stage.Id;
                    errMsg.Timestamp = DateTime.Now;
                    Context.Engine.Send(Request.ReturnAddress, errMsg);
                }
            }

            private void TrimBuffer()
            {
                if (Index + 1 < ChunkSize)
                {
                    var temp = new byte[Index + 1];
                    Array.Copy(Buffer, temp, temp.Length);
                    Buffer = temp;
                }
            }

            private void NextBuffer()
            {
                Buffer = new byte[ChunkSize];
                Index = 0;
            }

            private void SendChunk()
            {
                var chunk = new FileChunk
                {
                    Data = Buffer,
                    SequenceSize = ChunkCount,
                    SequenceNumber = ChunkNumber,
                    SequenceId = Request.IdForSequence,
                    CorrelationId = Request.IdForCorrelation
                };

                Context.Engine.Send(Request.ReturnAddress, chunk);

                ChunkNumber++;
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {   try
                    {
                        Stream.Dispose();
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex);
                    }
                }
                // if disposing is false, 
                // it means the world has gone wonky and
                // we somehow ended up in the finalizer
                // so we let the finalizer do what it does best,
                // and don't worry about the open stream...    
                base.Dispose(disposing);
            }
        }
    }

    [DataContract]
    public class FileReadRequest : RequestMessage
    {
        [DataMember]
        public string IdForSequence { get; set; }
        [DataMember]
        public string FileName { get; set; }
        [DataMember]
        public int ChunkSize { get; set; }
    }

    [DataContract]
    public class FileChunk : MessageBase, ICorrelation, ISequence
    {

        [DataMember]
        public string IdForCorrelation { get; set; }
        [DataMember]
        public string CorrelationId { get; set; }
        [DataMember]
        public string SequenceId { get; set; }
        [DataMember]
        public int SequenceNumber { get; set; }
        [DataMember]
        public int SequenceSize { get; set; }
        [DataMember]
        public byte[] Data { get; set; }

    }
}
